-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Update unreducible match types error reporting #19954
Conversation
This would revert the semantics of match types to what they were a long time ago. I believe there were good reasons, based on practical experience, to report an error on reducing-to-nothing match types. That was however long before I started looking into match types. Perhaps @OlivierBlanvillain has some more concrete recollections? In any case, this specific change would require a SIP at this point, unlike the other PR. |
Ah so I just found the PR #12768 which explicitly changed this. |
Yeah, I think the users are reasonable in asking for this error eagerly. |
736630d
to
646fea8
Compare
For reference, I played around a bit and found the following: With a simple case, we do get an error: type M[X] = X match
case 0 => 'a'
case 1 => 'b'
type R1 = M[2] // error selector matches none of the cases But with a setup like: trait A:
type X
type R = X match
case 0 => 'a'
case 1 => 'b'
trait B extends A:
type S = 2
type R2 = B#R // no error the matches with no cases go by undetected. Generally speaking, the rule can be circumvented with: type AllowNoMatchesM[X] = {
type X1 = X
type R = X1 match
case 0 => 'a'
case 1 => 'b'
}#R
type R3 = AllowNoMatchesM[2] // no error We could of course also consider this an error and try to fix it. But doing these kind of checks exhaustively seems complicated given the current implementation. I find getting these errors in unpredictable ways worse then getting no errors at all (the second example is actually minimised from something I ran into when working with the generic tuples and was quite confused about). |
Coming back to the original example (assuming the error is handled properly) #19949 (comment) val t: T[Double] = new T[Double] {}
val x: t.M = "hello" // error It also unclear to be reporting an error only on |
What is the difference between "no matches" and getting stuck? Is "no matches" where we know that the match type cannot reduce in any refining context? In that case, I think it's reasonable to throw a type error if we know the error will be reported. I believe this can be tested with ctx.typeAssigner.isInstanceOf[Typer] |
It's a pragmatic choice. The type system should not need any of this, but it is helpful to tell the user early that a match type makes no sense. |
Yes indeed, but it is possible for a refining context to yield a "stuck" match type, i.e. one that does not report an error.
Ah thanks ! I found |
Can you point me towards a scenario where that happens? |
Yes, a simplistic example is: type M[X] = X match
case 0 => 'a'
case 1 => 'b'
// type R1 = M[2] // error reduction failed since selector matches none of the cases
// but with a refined scrutinee:
type R2 = M[2 & 1] // no error because we do not try to reduce under an uninhabited scrutinee In practice, this does realistically occur from outer reductions or when working with match types as members, but errors don't get reported in predictable ways: #19954 (comment) I'm don't think keeping things as they are would be horrible either, just pointing out the current quirks with the design. |
Ah, so it's because it hits the uninhabited type scrutinee! Should that also throw a TypeError then? It seems desirable that the behavior is maintained under context refinement. |
Yes I agree ! I'm trying that out now to see how much of the tests are impacted. Edit: Implemented in #19964 |
646fea8
to
b665f45
Compare
8fb447c
to
3d7a77a
Compare
Why is bad-footprint removed? bad-footprint is an essential test. It's the only test we have that tests that match type reduction is consistent between contexts. That's the thing I have mentioned dozens of times already. |
I should probably have added comment here in addition to the commit message. Unfortunately, it used to only pass because of a missed |
Can we have a similar test case, but without opaque type aliases ? |
This reverts commit 9ae1598 Note that the changes in Typer: ``` val unsimplifiedType = result.tpe simplify(result, pt, locked) result.tpe.stripTypeVar match case e: ErrorType if !unsimplifiedType.isErroneous => errorTree(xtree, e.msg, xtree.srcPos) case _ => result ``` cannot be reverted yet since the MatchReducer now also reduces to an `ErrorType` for MatchTypeLegacyPatterns, introduced after 9ae1598.
i18488.scala was only passing because of the bug in the MatchReducer, as we can see in the subtyping trace: ``` ==> isSubType TableQuery[BaseCrudRepository.this.EntityTable] <:< Query[BaseCrudRepository.this.EntityTable, E[Option]]? ==> isSubType Query[BaseCrudRepository.this.EntityTable, Extract[BaseCrudRepository.this.EntityTable]] <:< Query[BaseCrudRepository.this.EntityTable, E[Option]] (left is approximated)? ==> isSubType E[Option] <:< Extract[BaseCrudRepository.this.EntityTable]? ==> isSubType [T[_$1]] =>> Any <:< Extract? ==> isSubType Any <:< Extract[T]? ==> isSubType Any <:< T match { case AbstractTable[t] => t } <: t (right is approximated)? ==> isSubType Any <:< <error Match type reduction failed since selector T matches none of the cases case AbstractTable[t] => t> (right is approximated)? <== isSubType Any <:< <error Match type reduction failed since selector T matches none of the cases case AbstractTable[t] => t> (right is approximated) = true <== isSubType Any <:< T match { case AbstractTable[t] => t } <: t (right is approximated) = true <== isSubType Any <:< Extract[T] = true <== isSubType [T[_$1]] =>> Any <:< Extract = true ... <== isSubType Extract[BaseCrudRepository.this.EntityTable] <:< E[Option] = true <== isSubType Query[BaseCrudRepository.this.EntityTable, Extract[BaseCrudRepository.this.EntityTable]] <:< Query[BaseCrudRepository.this.EntityTable, E[Option]] (left is approximated) = true <== isSubType TableQuery[BaseCrudRepository.this.EntityTable] <:< Query[BaseCrudRepository.this.EntityTable, E[Option]] = true ```
Modify the MatchReducer to return NoType in the case of no matches, rather than throwing a MatchTypeReductionError. This makes it consistent with the other match type reduction failures, where being stuck does not result in an error, but simply in an unreduced match type. We still get the explanations of the underlying error in the MatchTypeTrace, but in positions which need the reduction for conformance, rather than at application site of the match type.
The diff in neg/10349.scala is quite interesting. With a few intermediate values: ```scala type First[X] = X match case Map[_, v] => First[Option[v]] def first[X](x: X): First[X] = x match case x: Map[k, v] => val hdOpt: Option[v] = x.values.headOption first(hdOpt): First[Option[v]] // error only before changes ``` This now type-checks but will fail at runtime because of the in-exhaustivity of the match expression. Perhaps we should add some additional condition in `isMatchTypeShaped` to account for this, or at least emit a warning ?
`recurseWith` can be called with the same scrutinee (even if match type reduction is cached) if it is an applied match alias For example, `Tuple.Head[Tuple.Tail[T]]` will attempt to reduce `Tuple.Tail[T]` twice: - once as an argument of the match alias `Head`, and - once as a scrutinee in body of `Head` (after the substitution).
If a match type pattern is an opaque type, we use its bounds when checking the validity of the pattern. Following the ElimOpaque phase however, the pattern is beta-reduced (as normal applied type aliases), which may result in an illegal pattern.
3d7a77a
to
2beb67e
Compare
The PR is now based on #20017, allowing to keep |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice cleanup!
Match type reduction can fail for any of the following reasons:
Out of those, only Stuck and NoInstance, could get reduced in a refined context.
Status quo
The match reducer returns:
ErrorType
for NoMatches and LegacyPattern,NoType
, which implies the match type is left unreduced, in all other cases.In addition, the implementation has an issue where the
ErrorType
s can be left unreported, then entering the flexible type logic, thereby conforming to anything.Proposed changes
In addition to fixing the aforementioned bug, this PR proposes to leave all unreducible match types as unreduced.
Of course the reduction may be needed at a later point for conformance, in which case the error message will still contain the same explanations from the
MatchTypeTrace
.Fixes #19949
Fixes #19950
Discussion
All cases of failed match type reductions which we know will never reduce, even with refined scrutinee, should have a consistent behaviour. So NoMatches and EmptyScrutinee should either both be an error or both be left unreduced.
The current implementation attempts to do the former approach (but only for NoMatches), which has some limitations as discussed below (I'm not saying I can do better, hence the latter approach).
Undesirable errors
We dont always want an error for a NoMatches failed reduction, for example if we just need
Nothing
to conform to it:But we could do
def head: Nothing = ...
to avoid the error here.Generally speaking, places where the bounds of the match type suffice can still get a reduction error, and adding an ascription to avoid an inferred match type doesn't always do the trick.
Another refused example could be:
even though the function looks reasonable and type-checking would be sound.
Missed errors
Also note in the
EmptyTupleWrap
example, we get a reduction error from a match type application which does not appear in the source code. A valid question might be when and for what exactly these conditions are checked ?The goal is to report a type error early on for a NoMatches application right, but we are actually only doing so if we happen to do
tryNormalize
and end up in theMatchReducer
.Here is an example where were a match type with NoMatches is accepted
Generally speaking, the NoMatches error can be circumvented with:
Also note the projections are used in the examples for simplicity but are not necessary,
R
can be used withinB
as unreduced without a reported error.See #19799 for another example of inconsistent errors